﻿using System;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Actya.Core.Infrastructure;
using Actya.Core.Infrastructure.Events;
using Actya.Core.SiteStructure;
using Actya.Core.SiteStructure.Events;

namespace Actya.Web.Infrastructure
{
	public class CmsSiteMapProvider : XmlSiteMapProvider, IEventListener<SiteUpdated>, IEventListener<PagePublished>, IEventListener<PageUnPublished>
	{
		private readonly object _locker = new object();
		private bool _isBuilding;
		private SiteMapNode _rootNode;

		public override SiteMapNode  BuildSiteMap()
		{
			if (this._rootNode != null)
			{
				return this._rootNode;
			}
			if (this._rootNode == null && !this._isBuilding)
			{
				lock (_locker)
				{
					if (this._rootNode == null)
					{ 
						CreateSiteMap();
					}
				}
			}
			return this._rootNode;
		}

		protected override void Clear()
		{
			this._rootNode = null;
			base.Clear();
		}

		/// <summary>
		/// Event Listener for SiteUpdated events.
		/// </summary>
		/// <param name="message"></param>
		public void Handle(SiteUpdated message)
		{
			// It might be posiible that rebuilding the sitemap causes the SiteUpdated event. In that
 			// case we won't do anything.
			if (!_isBuilding)
			{
				// Clear the sitemap when something in the site changes. It will be re-populated on 
				// the next request.
				Clear();
			}
		}

		public void Handle(PagePublished payload)
		{
			Clear();
		}

		public void Handle(PageUnPublished payload)
		{
			Clear();
		}

		private void CreateSiteMap()
		{
			var siteService = DependencyResolver.Current.GetService<ISiteService>();
			var cmsContext = DependencyResolver.Current.GetService<ICmsContext>();

			this._isBuilding = true;

			// Get original sitemap from xml file.
			var rootNodeFromXmlSiteMap = base.BuildSiteMap(); 

			// Clone the original nodes, we're merging these with the nodes from the cms.
			var rootNodeFromXmlSiteMapClone = rootNodeFromXmlSiteMap.Clone();

			// We have a root node defined in XML. This root node must exist in the CMS root nodes collection and its 
			// child nodes must exist.
			var site = siteService.FindSiteByHostUrl(cmsContext.Host.Settings.HostUrl);
			EnsureXmlSiteMapNodesExistInCmsSitemap(rootNodeFromXmlSiteMapClone, site);
			siteService.UpdateSite(site);

			// Create a new sitemap based on the root node from the XML sitemap.
			AddSiteMapNodesToRootFromCms(rootNodeFromXmlSiteMapClone, site);

			this._isBuilding = false;
		}

		private void EnsureXmlSiteMapNodesExistInCmsSitemap(SiteMapNode rootNodeFromXmlSiteMap, Site site)
		{
			var cmsRootNode = site.SitemapNodes.SingleOrDefault(sn => sn.Url == rootNodeFromXmlSiteMap.Url);
			if (cmsRootNode == null)
			{
				// Create a new root node in the CMS sitemap
				cmsRootNode = CreateCmsNodeFromAspNetNode(rootNodeFromXmlSiteMap);
				site.SitemapNodes.Insert(0, cmsRootNode);
			}
			// Ensure children exist.
			SyncChildNodes(cmsRootNode, rootNodeFromXmlSiteMap.ChildNodes);
		}

		private void SyncChildNodes(SitemapNode cmsNode, SiteMapNodeCollection aspNetChildNodes)
		{
			foreach (SiteMapNode aspNetChildNode in aspNetChildNodes)
			{
				var cmsChildNode = cmsNode.SitemapNodes.FirstOrDefault(cn => cn.Url == aspNetChildNode.Url);
				if (cmsChildNode == null)
				{
					cmsChildNode = CreateCmsNodeFromAspNetNode(aspNetChildNode);
					cmsChildNode.Parent = cmsNode;
					cmsNode.SitemapNodes.Add(cmsChildNode);
				}
				SyncChildNodes(cmsChildNode, aspNetChildNode.ChildNodes);
			}
		}

		private void AddSiteMapNodesToRootFromCms(SiteMapNode rootNodeFromXmlSiteMap, Site site)
		{	
			this._rootNode = rootNodeFromXmlSiteMap;

			var cmsRootNode = site.SitemapNodes.FirstOrDefault(sn => sn.Url == rootNodeFromXmlSiteMap.Url);
			if (cmsRootNode != null)
			{
				AddChildNodesToSiteMap(cmsRootNode, this.RootNode);
			}
		}

		private void AddChildNodesToSiteMap(SitemapNode cmsNode, SiteMapNode siteMapNode)
		{
			var pageService = DependencyResolver.Current.GetService<IPageService>();
			// Check security and publication status before adding to sitemap.
			var sitemapNodes = cmsNode.SitemapNodes;
			var allPageIds = sitemapNodes.Where(sn => sn.PageId != null).Select(sn => sn.PageId);
			var filteredPageIds = pageService.GetPublishedAndAuthorizedPageIds(allPageIds.ToArray());
			var filteredSitemapNodes = sitemapNodes.Where(sn => sn.PageId == null || filteredPageIds.Contains(sn.PageId) && sn.ShowInNavigation);

			foreach (var cmsChildNode in filteredSitemapNodes)
			{
				SiteMapNode newNode;
				var existingNode = siteMapNode.ChildNodes.OfType<SiteMapNode>().FirstOrDefault(smn => smn.Url == cmsChildNode.Url);
				if (existingNode != null)
				{
					RemoveNode(existingNode);
					newNode = existingNode;
				}
				else
				{
					newNode = new SiteMapNode(this, cmsChildNode.Id, cmsChildNode.Url, cmsChildNode.Title);
				}
				AddNode(newNode, siteMapNode);
				AddChildNodesToSiteMap(cmsChildNode, newNode);
			}
		}

		private SitemapNode CreateCmsNodeFromAspNetNode(SiteMapNode rootNodeFromXmlSiteMap)
		{
			var newNode = new SitemapNode();
			newNode.Url = rootNodeFromXmlSiteMap.Url;
			newNode.Title = rootNodeFromXmlSiteMap.Title;
			return newNode;
		}
	}
}